package com.justgame.project.cloud.common.util;


import com.alibaba.fastjson.JSONObject;
import com.aliyun.oss.OSS;
import com.aliyun.oss.model.OSSObject;
import com.justgame.project.cloud.base.entity.FileSizeUnit;
import com.justgame.project.cloud.base.entity.OssFile;
import com.justgame.project.cloud.common.constant.Const;
import com.justgame.project.cloud.common.constant.Regex;
import com.justgame.project.cloud.common.enums.FileTypeEnum;
import com.justgame.project.cloud.common.exception.Exc;
import lombok.extern.slf4j.Slf4j;
import net.coobird.thumbnailator.Thumbnails;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletRequest;
import java.io.*;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.stream.Collectors;

/**
 * 文件工具类
 */
@Slf4j
@Component
public class FileUtils {

    private static volatile String bucketName;

    @Value("${bucket}")
    public void setBucketName(String bucketName){
        FileUtils.bucketName = bucketName;
    }

    public static Object readOssFile(String fileUrl) {
        OSS staticOss = AppContextUtils.getBean(OSS.class);
        boolean isExist = staticOss.doesObjectExist(AliyunOssUtil.getBucket(), AliyunOssUtil.getOssFileName(fileUrl));
        if (!isExist) throw new Exc("该文件不存在！");
        OSSObject object = staticOss.getObject(AliyunOssUtil.getBucket(), AliyunOssUtil.getOssFileName(fileUrl));
        StringBuilder sb = new StringBuilder();
        InputStream inputStream = null;
        try {
            inputStream = object.getObjectContent();
            int temp;
            while ((temp = inputStream.read()) != -1) {
                /* 解决多行空格 */
                if (((char) temp) != '\r') {
                    sb.append((char) temp);
                }
            }
            inputStream.close();
            object.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return JSONObject.parseObject(sb.toString());
    }

    public static Object readLocalFile(String filePath) {
        File file = new File(filePath);
        if (!file.exists()) {
            return null;
        }
        StringBuilder sb = new StringBuilder();
        Reader reader = null;
        try {
            reader = new InputStreamReader(new FileInputStream(file));
            int temp;
            while ((temp = reader.read()) != -1) {
                /* 解决多行空格 */
                if (((char) temp) != '\r') {
                    sb.append((char) temp);
                }
            }
            reader.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
            return null;
        } catch (IOException e) {
            e.printStackTrace();
        }
        return JSONObject.parseObject(sb.toString());
    }

    public static void deleteFile(String path) {
        RedisUtils redis = AppContextUtils.getBean(RedisUtils.class);
        if (Regex.isUrl(path)) {
            AppContextUtils.getBean(OSS.class).deleteObject(AliyunOssUtil.getBucket(), AliyunOssUtil.getOssFileName(path));
            /* 从Redis中删除临时信息 */
            if (redis.hExists(Const.KEY_TEMP_FILES, path)) {
                redis.hDelete(Const.KEY_TEMP_FILES, path);
            }
        } else {
            String fullPath = (Const.LOCAL_PATH + path).replaceAll("\\\\", "/");
            log.info("\n正在删除{}", fullPath);
            File file = new File(Const.LOCAL_PATH + path);
            if (file.exists()) {
                log.info("该文件存在！正在执行删除操作！");
                log.info("大小：{}，是否是文件：{}，可执行：{}，可写：{}，可读：{}",
                        file.length(), file.isFile(), file.canExecute(), file.canWrite(), file.canRead()
                );
                boolean success = file.delete();
                log.info("文件删除{}", success ? "成功" : "失败");
            }
        }
    }

    public static void deleteFiles(Collection<String> paths) {
        for (String path : paths) {
            deleteFile(path);
        }
    }

    private static void safeMkdir(String path) {
        //检查目录
        File uploadDir = new File(path);
        if (!uploadDir.exists()) {
            uploadDir.mkdir();
        }
        if (!uploadDir.canWrite()) {
            uploadDir.setWritable(true);
        }
    }


    /**
     * 文件大小数字转字符串
     *
     * @param len long
     * @return String
     * @author ZhouWeiBo
     */
    public static String getFileSizeStr(long len) {
        String fileSize = "0b";
        if (len >= 0 && len < 1024) {
            fileSize = (double) len + "b";
        } else if (len >= 1024 && len < 1048576) {
            fileSize = ((double) len / 1024) + "kb";
        } else if (len >= 1048576 && len < 1073741824) {
            fileSize = ((double) len / 1048576) + "mb";
        } else if (len > 1073741824) {
            fileSize = ((double) len / 1073741824) + "gb";
        }
        return fileSize;
    }

    public static boolean isFileLargerThanSize(File file, FileSizeUnit unit, long size) {
        long fileSizeInBytes = file.length();
        long fileSizeInUnit = getFileSizeInUnit(fileSizeInBytes, unit);
        return fileSizeInUnit > size;
    }

    private static long getFileSizeInUnit(long fileSizeInBytes, FileSizeUnit unit) {
        return fileSizeInBytes / unit.getSize();
    }

    /**
     * 获取文件保存路径中的文件后缀(默认小写)
     *
     * @param fileUrl
     * @return
     */
    public static String getFileUrlSuffix(String fileUrl) {
        if (StringUtils.isBlank(fileUrl)) return "";
        fileUrl = fileUrl.toLowerCase();
        try {
            if (Regex.isUrl(fileUrl)) {
                String fileName = fileUrl.substring(fileUrl.lastIndexOf("/") + 1);
                if (fileName.contains(".")) {
                    return fileName.substring(fileName.lastIndexOf(".") + 1).toLowerCase();
                }
            } else if (fileUrl.contains(".")) {
                return fileUrl.substring(fileUrl.lastIndexOf(".") + 1).toLowerCase();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return "";
    }

    /**
     * 保存文件方法
     *
     * @param uploadFiles  文件对象
     * @param typeEnum     文件类型
     * @param MaxFileCount 最大上传数量
     * @return
     */
    public static List<Map<String, Object>> saveFiles(MultipartFile[] uploadFiles, FileTypeEnum typeEnum, int MaxFileCount, long MaxFileSize) {
        /* 获取请求体和响应体 */
        HttpServletRequest request = HttpContextUtil.getHttpRequest();

        List<Map<String, Object>> files = new ArrayList<>();
        /* init */
        MaxFileCount = MaxFileCount == 0 ? 1 : MaxFileCount;
        typeEnum = typeEnum == null ? FileTypeEnum.File : typeEnum;


        if (uploadFiles.length == 0) {
            throw new Exc("未选择文件！");
        } else if (uploadFiles.length > MaxFileCount) {
            throw new Exc(String.format("上传文件数量不能大于%s", MaxFileCount));
        }

        // 文件保存目录路径
        String savePath = Const.LOCAL_PATH,
                //文件保存目录URL
                saveUrl = request.getContextPath();

        //判断客户端<form>标记的enctype属性是否是“multipart/form-data"
        if (!ServletFileUpload.isMultipartContent(request)) {
            throw new Exc("未选择文件！");
        }

        //检查目录
        safeMkdir(savePath);

        //创建年月日目录，
        String ymd = new SimpleDateFormat("yyyyMMdd").format(new Date());
        savePath += Const.UserFilesPath + ymd + "/";
        saveUrl += Const.UserFilesPath + ymd + "/";
        //判断当天目录是否存在，不存在则创建文件夹
        safeMkdir(savePath);

        int skipCount = 0;
        for (MultipartFile uploadFile : uploadFiles) {
            Map<String, Object> file = new HashMap<String, Object>();
            /* 如果文件过大，跳过保存此文件 */
            if (uploadFile.getSize() > MaxFileSize) {
                skipCount += 1;
                log.error("已跳过保存文件:{}", getFileSizeStr(uploadFile.getSize()));
                continue;
            }

            log.info("正在上传文件，占据存储空间：{}", getFileSizeStr(uploadFile.getSize()));

            // 如果当前上传的文件不为空
            if (!uploadFile.isEmpty()) {

                // 获取上传文件的名字
                String fileName = uploadFile.getOriginalFilename();
                if (StringUtils.isBlank(fileName)) {
                    throw new SecurityException("未知文件名，无法上传！");
                }

                file.put("fileName", fileName);
                file.put("fileSize", uploadFile.getSize());

                // 截取扩展名 toLowerCase用于将大写字符转换为小写  +1表示不包括.开始截取

                String fileExt = FileUtils.getFileUrlSuffix(fileName);

                // 校验该扩展名是否在该键对应的格式中 contains:当且仅当此字符串包含指定的char值序列时，返回true
                if (!typeEnum.isContains(fileExt)) {
                    throw new Exc("上传文件扩展名是不允许的扩展名。\n只允许" + Arrays.toString(typeEnum.getSuffix()) + "格式。");
                }
                file.put("fileSuffix", fileExt);

                String newFileName = String.format("%s.%s", IdUtils.create(), fileExt);
                try {
                    FileOutputStream os = new FileOutputStream(savePath + newFileName);
                    IOUtils.copy(uploadFile.getInputStream(), os);
                    os.close();
                } catch (Exception e) {
                    throw new Exc("上传文件失败。");
                }
                file.put("filePath", saveUrl + newFileName);
                files.add(file);
            }
        }
        if (skipCount > 0) {
            throw new Exc("文件过大，上传失败！");
        }
        return files;
    }

    /**
     * 此方法使用情景为：
     * 用户上传一个文件，该文件的url会存在于redis中记录并返回给前端
     * 若用户将上一步服务器返回的文件url提交到保存接口，并保存成功后则删除redis中该条记录
     * 若在时效期内不删除该条url记录，则会被定时任务捕获并向OSS提交删除该文件命令及相关后续操作
     *
     * @param request
     */
    @Async("taskExecutor")
    public void asyncClearFileTask(ServerHttpRequest request) {
        ServletServerHttpRequest req = (ServletServerHttpRequest) request;
        HttpServletRequest httpServletRequest = req.getServletRequest();
        Map<String, Object> jsonObj = JSONObject.parseObject(HttpContextUtil.getBodyString(httpServletRequest));
        if (jsonObj != null) {
            jsonObj.forEach((key, value) -> {
                checkDownObj(value);
            });
        }
    }

    private void checkDownObj(Object value) {
        RedisUtils redisUtils = AppContextUtils.getBean(RedisUtils.class);
        if (value instanceof String && StringUtils.isNotBlank((String) value) && Regex.isUrl((String) value)) {
            if (redisUtils.hExists(Const.KEY_TEMP_FILES, value)) {
                redisUtils.hDelete(Const.KEY_TEMP_FILES, value);
                log.info("\n已删除临时文件{}", value);
            }
        } else if (value instanceof Collection) {
            List<Object> values = new ArrayList<>((Collection) value);
            values.forEach(this::checkDownObj);
        } else if (value instanceof Map) {
            ((Map<String, Object>) value).forEach((k, v) -> {
                checkDownObj(v);
            });
        }
    }

    public static OssFile upload(MultipartFile uploadFile, FileTypeEnum typeEnum, FileSizeUnit unit,int numberOfUnit, boolean isCompression) {
        return upload(new MultipartFile[]{uploadFile},typeEnum,1,unit.getSize() * numberOfUnit,false).stream().findFirst().orElseThrow();
    }
    /**
     * 阿里云上传OSS对象
     *
     * @param uploadFiles   文件对象
     * @param typeEnum      文件类型
     * @param MaxFileCount  最大上传数量
     * @param isCompression 是否开启压缩
     */
    public static List<OssFile> upload(MultipartFile[] uploadFiles, FileTypeEnum typeEnum, int MaxFileCount, long MaxFileSize, boolean isCompression) {
        MaxFileCount = MaxFileCount == 0 ? 1 : MaxFileCount;
        Assert.notEmpty(uploadFiles, "未选择文件！");
        Assert.isTrue(uploadFiles.length <= MaxFileCount, "上传文件数量不能大于" + MaxFileCount);
        List<OssFile> filesMap = Arrays.stream(uploadFiles)
                .filter(file -> file.getSize() <= MaxFileSize)
                .map(file -> {
                    try (InputStream inputStream = isCompression ? new ByteArrayInputStream(compressImage(file, 0.8f)) : file.getInputStream()) {
                        // 获取上传文件的名字
                        String fileName = file.getOriginalFilename();
                        Assert.hasText(fileName, "未知文件名，无法上传！");
                        // 截取扩展名
                        String suffix = getFileUrlSuffix(fileName);
                        // 校验该扩展名是否在该键对应的格式中
                        Assert.isTrue(typeEnum == FileTypeEnum.System || typeEnum.isContains(suffix),
                                "上传文件扩展名是不允许的扩展名。\n只允许" + Arrays.toString(typeEnum.getSuffix()) + "格式。");
                        String ossFullName = AliyunOssUtil.buildOssFileName(typeEnum, String.format("%s.%s", IdUtils.create(), suffix));
                        AppContextUtils.getBean(OSS.class).putObject(FileUtils.bucketName, ossFullName, inputStream);
                        String ossFileName = String.format("https://%s.%s/%s", Const.OSS_BUCKET_NAME, Const.OSS_ENDPOINT, ossFullName);
                        return OssFile.builder()
                                .name(fileName)
                                .size(file.getSize())
                                .suffix(suffix)
                                .path(ossFileName)
                                .build();
                    } catch (IllegalArgumentException e){
                        throw new Exc(e.getMessage());
                    } catch (Exception e) {
                        e.printStackTrace();
                        throw new Exc("文件上传失败");
                    }
                })
                .collect(Collectors.toUnmodifiableList());
        Assert.isTrue(filesMap.size() == uploadFiles.length, "文件过大，上传失败！");
        return filesMap;
    }

    public static byte[] compressImage(MultipartFile file,double quality) throws IOException {
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        Thumbnails.of(file.getInputStream())
                .scale(1f) //图片大小（长宽）压缩比例 从0-1，1表示原图
                .outputQuality(quality) //图片质量压缩比例 从0-1，越接近1质量越好
                .toOutputStream(outputStream);
        return outputStream.toByteArray();
    }
}
